Skip to content

feat(gov): ops hero, activity, confidence signals, metadata chips (PR 6c)#9

Merged
sidhujag merged 5 commits intomasterfrom
gov-vote-analytics
Apr 21, 2026
Merged

feat(gov): ops hero, activity, confidence signals, metadata chips (PR 6c)#9
sidhujag merged 5 commits intomasterfrom
gov-vote-analytics

Conversation

@sidhujag
Copy link
Copy Markdown
Member

Summary

Third (and final) UX pass on the governance surface. PR 6a brought receipts-aware defaults; PR 6b added grouped buckets and exhaustive error copy. 6c closes the loop on situational awareness — a signed-in voter can now see, at a glance:

  1. Their stake in the current cycle — how many proposals they represent and need to vote on (ops hero).
  2. What their vault has already done — a short log of recent receipts with deep-link-back-to-row (activity card).
  3. Whether their next vote will count — a freshness pill on confirmed rows and a pre-submit net-support preview in the modal.
  4. Which proposals deserve attention right now — closing-window / over-budget / slim-margin chips derived from feed-wide state.

What changed

  • Ops summary hero (`GovernanceOpsHero` + `lib/governanceOps.js`): represent / need-vote / passing counts, progress bar, `Jump to next unvoted` CTA.
  • Your activity card (`GovernanceActivity`): last 10 receipts via `GET /gov/receipts/recent`. Per-row outcome/status/relative-time + jump-to-proposal. Loading / empty / error states with inline retry.
  • Confidence signals:
    • `verified-chip` on `ProposalRow` — only when the reconciler has fresh (<5 min) confirmed receipts for this user on this proposal.
    • Pre-submit support-shift preview in `ProposalVoteModal` (`lib/governanceSupportShift.js`): computes net `AbsoluteYesCount` delta from selected MNs vs. their prior confirmed votes. Tones are positive / negative / neutral; detail line calls out confirmed-replacement count so a vote change never happens silently.
  • Metadata chips (`lib/governanceMeta.js`):
    • `closing-soon` / `closing-urgent` — superblock voting deadline ≤ 7d / ≤ 48h.
    • `over-budget` — cumulative passing payouts rank-cut against the superblock budget.
    • `margin-thin` / `margin-near` — support within ±1.5% of the 10% pass line.
  • New `GET /gov/receipts/recent` wired into `governanceService.fetchRecentReceipts` with client-side validation on `limit`.

Companion PR

Backend: syscoin/sysnode-backend#` `gov-vote-analytics` — adds `GET /gov/receipts/recent`.

Tests

474 / 474 passing (33 suites). New suites cover:

  • ops stats helper + hero component
  • activity list + deep-link jump behaviour
  • support-shift helper + modal integration (fresh / flipped / abstain / no-op)
  • verified pill freshness gating (fresh / stale / no-confirmed / anon)
  • metadata chips (closing tiers, over-budget rank cut, margin bands)

Not in this PR

  • Live polling for the verified pill (currently freshness-gated on last reconcile tick — good enough for a passive confidence signal).
  • Richer popover for cohort / metadata chips (still native `title`; leaving for a later pass once we know which tooltips users actually hover).

Test plan

  • Sign in; open `/governance`. Confirm ops hero renders with correct represent/need-vote counts; `Jump to next` scrolls + highlights the expected row.
  • Cast a vote; confirm the activity card updates on modal close and its jump buttons scroll back to the correct row.
  • Open the vote modal on a proposal where you have a confirmed yes; flip outcome to no; confirm `−N` shift preview with replacement detail.
  • Inside 48h of a superblock deadline on mainnet, confirm `Closes in Xh` urgent chip.
  • With two passing proposals whose combined budget exceeds the ceiling, confirm the lower-ranked one picks up the `Over budget` chip.

Made with Cursor

…s (PR 6c)

Third UX pass on the governance surface. Closes the loop on situational
awareness: a signed-in voter can now see at a glance what they represent,
what their vault has already done, whether their next vote will count,
and which proposals deserve attention right now.

- GovernanceOpsHero + lib/governanceOps: represent / need-vote / passing
  counts, progress bar, Jump-to-next-unvoted CTA.
- GovernanceActivity: last-10 receipts card with jump-to-row, backed by
  the new GET /gov/receipts/recent endpoint (governanceService
  .fetchRecentReceipts).
- Confidence signals:
  * verified-chip on ProposalRow, only when the reconciler has fresh
    (<5 min) confirmed receipts for this user on this proposal.
  * Pre-submit support-shift preview in the vote modal (lib/
    governanceSupportShift): net AbsoluteYesCount delta from selected
    MNs vs. prior confirmed votes, with replacement count called out
    in the detail line.
- Metadata chips (lib/governanceMeta): closing-soon / closing-urgent
  tiers off the superblock voting deadline, over-budget rank cut
  against the superblock budget, and margin-thin / margin-near within
  ±1.5% of the 10% pass line.

Made-with: Cursor
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 518e93acd6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/governanceMeta.js Outdated
(Number(proposal.AbsoluteYesCount || 0) / enabled) * 100;
const delta = support - PASSING_SUPPORT_PERCENT;
if (Math.abs(delta) > MARGIN_WARNING_PERCENT) return null;
const above = delta >= 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify exactly-10% support as near-passing

The margin chip currently treats delta === 0 as the “above threshold” branch, so a proposal at exactly 10% support gets the margin-thin copy (“just above the 10% pass threshold”) even though the row’s pass logic uses support > 10 and still marks it as not passing. This creates contradictory status signals at the threshold boundary; use a strict delta > 0 check (or a dedicated exact-threshold label) so chip semantics match the actual pass condition.

Useful? React with 👍 / 👎.

Comment thread src/pages/Governance.js
Comment on lines +348 to +350
for (const p of proposals) {
if (p && typeof p.Key === 'string') {
m.set(p.Key.toLowerCase(), p);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Build activity jump lookup from visible proposals

The activity card’s jump affordance is keyed from proposalsByHash, but that map is built from the full proposals list while the table renders visibleProposals. When a user applies a search/filter that hides a proposal they previously voted on, the activity row still renders a jump button, but clicking it cannot scroll to any DOM row because that row is not mounted. This produces a silent no-op CTA in normal filtered views.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this is re-flagging the same concern after the round-1 fix. I chose to keep the map built from the full proposals list on purpose and address the silent-no-op failure mode at the click site instead:

jumpToProposal (Governance.js ~420) now detects when the target hash exists in the feed but is hidden by the current filter/query, clears filter + query, and scrolls inside a requestAnimationFrame so React commits the filter reset before getElementById runs. The two tests in Governance.test.js under "jumpToProposal filter-aware behaviour" pin this contract: hidden target → filter clears and row mounts; visible target → filter is preserved (no silent reset).

Filtering the activity card's jump map to only currently-visible proposals is the alternative, but it's strictly worse UX in this context — activity should reflect the user's real voting history regardless of what view filter is currently applied, and hiding jump buttons for activity rows behind an unrelated filter was the original "silent dead-end" complaint in a different shape. Happy to revisit if there's a scenario I'm missing.

- marginChip: use strict delta>0 at the 10% line so exactly-10%
  support reads as "Close to passing" instead of "Slim margin"
  (ProposalRow's pass check is `support > 10`, so contradictory
  copy at the boundary was confusing).
- jumpToProposal: when the target proposal exists in the feed
  but is hidden by the current search/filter, clear filter+query
  first so the row is mounted before we scroll. Scrolling runs
  inside requestAnimationFrame so React has a chance to commit
  the filter reset before getElementById fires. Prevents the
  silent no-op CTA on filtered activity rows.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d90e7b640

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/governanceMeta.js Outdated

let running = 0;
for (const row of passing) {
running += Math.max(0, row.amount);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard over-budget accumulation against NaN amounts

In computeOverBudgetMap, a malformed payment_amount (for example a non-numeric string from the feed) makes row.amount become NaN, and running += Math.max(0, row.amount) then turns running into NaN. After that, running > ceiling is always false, so lower-ranked passing proposals stop getting over-budget chips even when they should. Coercing non-finite amounts to 0 before adding preserves correct cutline detection for the rest of the list.

Useful? React with 👍 / 👎.

Codex re-raised the "silent no-op jump CTA under an active filter"
concern against the filter-clearing fix. The jumpToProposal
callback already resets filter+query when the target proposal
exists in the feed but is hidden by the current filter; this
commit adds explicit coverage so the behaviour is a pinned
contract and future readers don't re-litigate the approach:

- hidden target → filter state clears, row becomes mountable
- visible target → filter state is preserved (no silent reset)

The stubs for GovernanceOpsHero / GovernanceActivity expose the
onJumpToProposal prop via a test-only button so these page-level
tests don't need the real activity fetch.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

…(Codex round 2)

A malformed payment_amount (non-numeric string from the feed)
coerces to NaN, which then contaminates the running budget
total. Once `running` is NaN every `running > ceiling` check
returns false, so the over-budget chip silently disappears from
downstream rows that should get it.

Clamp non-finite amounts to 0 before adding. Biases toward "no
warning" on an unknowable row rather than warning spuriously,
while keeping cutline detection working for the rest of the list.

Made-with: Cursor
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e1c4cad7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/Governance.js Outdated
Comment on lines +385 to +389
() =>
closingChip({
votingDeadline: superblockStats ? superblockStats.voting_deadline : 0,
}),
[superblockStats]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute closing chip from current time

The closing metadata chip is memoized only on superblockStats, but closingChip() depends on Date.now(). After initial render, the label/kind can stay frozen for the whole session (for example, it never transitions from closing-soon to closing-urgent, and may remain visible after the deadline) unless the stats object itself is refreshed. This produces stale urgency signals for long-lived governance sessions.

Useful? React with 👍 / 👎.

@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98277e2c22

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/Governance.js
Comment on lines +384 to +388
const closing = useMemo(
() =>
closingChip({
votingDeadline: superblockStats ? superblockStats.voting_deadline : 0,
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute closing chip as wall-clock time advances

closingChip depends on Date.now(), but this useMemo only invalidates when superblockStats changes, so the “Closes in …” label and urgent/soon tier are frozen to the initial render in any long-lived tab. In practice, users can keep seeing stale countdowns (or miss escalation to urgent/expired) until a manual feed refresh occurs, which makes the new deadline signal inaccurate during normal use.

Useful? React with 👍 / 👎.

…ssions (Codex round 2)

The closing-window chip (and the verified-on-chain freshness
check) both depend on Date.now(), but the derivations were
memoized only on the stats object. That means on a tab that
stays open past the chip's boundary:
- "closing-soon" (7d window) never escalated to "closing-urgent"
  at the 48h line;
- the chip never disappeared once the deadline passed;
- the verified-chip freshness window (5 min) could linger even
  after staleness.

Fix: add a slow-ticking nowMs state at the page level (one
update per minute), thread it into the closingChip memo and into
ProposalRow so the verified pill uses the same clock. The tick
interval is coarse on purpose — chip copy rounds to m/h/d so
sub-minute drift is invisible, and 60 re-renders per hour on a
quiet page is cheap. Any hard-timed behaviour (server-side
deadline enforcement) is unaffected.

Adds two tests that advance wall-clock + timers without touching
the stats object: soon→urgent escalation, and chip vanishing
after the deadline.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sidhujag sidhujag merged commit ad89e15 into master Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant